//
// C++ Implementation: activityplot
//
// Description:
//
//
// Author: Willi Ballenthin <wilbal1087@gmail.com>, (C) 2008
//
// Copyright: See COPYING file that comes with this distribution
//
//
#include "activityplot.h"
#include <QDebug>

/**
 * Constructor.
 * @param interface A reference to the database interface from which to get the model.
 * @param property The type of plot (distance, duration, effort).
 * @param parent The parent graphics item.
 */
ActivityPlot::ActivityPlot ( ActivityDbInterface* interface,
                             QString property,
                             QGraphicsItem *parent )
        :  QGraphicsItem ( parent )
{
    // interface should be set before property, since property
    // calls updatemodel which calls the model/interface.
    this->interface = interface;
    setAcceptHoverEvents ( true );

    setProperty ( property );
}


/**
 * Constructor. Defaults the plot type to distance.
 * @param interface A reference to the database interface from which to get the model.
 * @param parent The parent graphics item.
 */
ActivityPlot::ActivityPlot ( ActivityDbInterface* interface,
                             QGraphicsItem *parent )
        :  QGraphicsItem ( parent )
{
    this->interface = interface;
    setAcceptHoverEvents ( true );

    setProperty ( "distance" );
}

/**
* This plot should have coordinates of 512 by 256
*/
QRectF ActivityPlot::boundingRect() const
{
    return QRectF ( 0, 0, TOTALWIDTH, TOTALHEIGHT );
}



/**
 * Sets the type of plot, but is limited to variables that make sense.
 * @param property The variable to plot (distance, duration, effort).
 */
void ActivityPlot::setProperty ( QString property )
{
    if ( property.compare ( "distance", Qt::CaseInsensitive ) != 0 &&
            property.compare ( "duration", Qt::CaseInsensitive ) != 0 &&
            property.compare ( "effort", Qt::CaseInsensitive ) != 0 )
    {
        qWarning ( "this property cannot be plotting. Reverting to Distance." );
        property = "distance";
    }
    this->property = property;
    updateModel();
}

/**
* Accessor.
* @Return The property currently represented by the plot.
*/
QString ActivityPlot::getProperty()
{
    return property;
}

float ActivityPlot::getProperty ( Activity act )
{
    float propValue;

    if ( property.compare ( "distance", Qt::CaseInsensitive ) == 0 )
    {
        propValue = act.getDistance();
    }

    else if ( property.compare ( "duration", Qt::CaseInsensitive ) == 0 )
    {
        // convert time value to float value
        QTime duration = act.getDuration();
        propValue =  duration.hour() * 10;
        propValue += ( float ) duration.minute() / 6;
        propValue += ( float ) duration.second() / 360;
    }

    else if ( property.compare ( "effort", Qt::CaseInsensitive ) == 0 )
    {
        propValue = act.getEffort();
    }

    else
    {
        qWarning ( "this property cannot be plotting. Reverting to Distance." );
        property = "distance";
        propValue = act.getDistance();
    }

    return propValue;
}


/**
* Slot that handles the case in which the model is updated.
* A new map is created to translate between the model and
* the plot, and then some statistics are generated.
*/
// TODO: this section is really messy and long. confusing.
void ActivityPlot::updateModel()
{
    Activity act;
    int i;
    max = avg = sum = 0;

    mapper.clear();
    vector.clear();
    vector = * ( interface->getModel() );

    /// create a map

    for ( i = 0; i < vector.size(); i++ )
    {
        act = vector.value ( i );
        float propValue = getProperty ( act );

        // only add activities whose values are useful
        if ( propValue > 0 )
        {
            ActivityPlotMap map;
            map.index = i;
            map.date = act.getDate();
            map.propValue = propValue;
            mapper.append ( map );
        }
    }

    // TODO: this assumes that the dates are in order by date
    dateStart = mapper.first().date;
    dateEnd   = mapper.last().date;
    days      = dateStart.daysTo ( dateEnd );
    xInterval = ( float ) ( PLOTRIGHT - PLOTLEFT ) / days;
    min = mapper.value ( 0 ).propValue;

    /// consolidate doubles and triples
    /// also, figure out some statistics
    //TODO: this is a mess, 4 levels of logic!!
    for ( i = 0; i < mapper.size(); i++ )
    {
        // if multiple activities took place on the same day combine them.
        while ( mapper.value ( i ).date.daysTo ( mapper.value ( i + 1 ).date ) == 0
                && i < mapper.size() - 1 )
        {
            ActivityPlotMap* mapData = mapper.data();
            mapData += i;
            // if were plotting effort, plot only the highest effort
            if ( property.compare ( "effort", Qt::CaseInsensitive ) == 0 )
            {
                // pick the max
                if ( ( mapData + 1 )->propValue >  mapData->propValue )
                {
                    mapper.remove ( i );
                }
                // make sure we keep moving along
                else
                {
                    i++;
                }
            }
            // otherwise, sum the values and continue
            else
            {
                mapData->propValue += ( mapData + 1 )->propValue;
                mapper.remove ( i + 1 );
            }
        }

        /// figure out max and min
        float temp = mapper.value ( i ).propValue;
        sum += temp;
        if ( temp > max )
        {
            max = temp;
        }
        else if ( temp < min )
        {
            min = temp;
        }
    }
    /// db interface current makes sure only result sets > 0 are returned.
    /// if this changes, then...theres a problem here, divide by zero
    avg = sum / vector.size();

    /// recalcuate the plot values for each point
    ActivityPlotMap* mapData = mapper.data();
    for ( i = 0; i < mapper.size(); i++ )
    {
        ( mapData + i )->setY ( PLOTBOTTOM, max );
        ( mapData + i )->setX ( mapper.first().date, xInterval );
    }
}


void ActivityPlot::paint ( QPainter *painter, const QStyleOptionGraphicsItem* /*option*/,
                           QWidget* /* widget */ )
{
    QTime time;
    time.start();

    QPainterPath plotPath;
    int i;
    /// add subpaths to the plot path
    /// which contain only consecutive activities
    // this relies upon the return of a default-constructed value
    // when i reaches mapper.size()
    // this should not be a problem when looking for the max or to add
    // values, as long as the default constructor of the Activity object
    // initializes values to 0
    for ( i = 0; i < mapper.size(); i++ )
    {
        QVector<ActivityPlotMap> group;
        group.append ( mapper.value ( i ) );
        // find a consecutive set of activities
        while ( mapper.value ( i + 1 ).day - mapper.value ( i ).day < 2
                && i < mapper.size() - 1 )
        {
            group.append ( mapper.value ( i + 1 ) );
            i++;
        }
        subpathGroup ( &plotPath, group );
    }

    paintBg ( painter );

    /// draw some statistics
    // draw in transparent white
    QPen pen ( QColor::fromRgbF ( 1, 1, 1, .2 ) );
    painter->setPen ( pen );

    // draw average

    float avgValue = PLOTBOTTOM - avg * PLOTBOTTOM / max;
    QPointF avgPtZero ( PLOTLEFT,  avgValue );
    QPointF avgPtMax ( TOTALWIDTH, avgValue );
    painter->drawLine ( avgPtZero, avgPtMax );
    drawValue ( painter, PLOTRIGHT + 10, avgValue, avg );

/*
    // draw min
    float minValue  = PLOTBOTTOM - min * PLOTBOTTOM / max;
    QPointF minPtZero ( PLOTLEFT, minValue );
    QPointF minPtMax ( TOTALWIDTH, minValue );
    painter->drawLine ( minPtZero, minPtMax );
    drawValue ( painter, PLOTRIGHT + 10, minValue, min );
*/

    // draw white foundation of plot
    painter->setBrush ( Qt::white );
    pen = QPen ( QColor::fromRgbF ( 0, 0, 0, .1 ) );
    pen.setCapStyle ( Qt::RoundCap );

    // draw a wide drop shadow
    pen.setWidth ( 6 );
    painter->setPen ( pen );
    painter->drawPath ( plotPath );

    // draw a narrower drop shadow
    pen.setWidth ( 3 );
    painter->setPen ( pen );
    painter->drawPath ( plotPath );

    /// draw the groups
    // gradient from bottom to top
    QLinearGradient gradient ( 0, 0, 0, PLOTBOTTOM );
    // darker blue
    gradient.setColorAt ( 0, QColor::fromHsv ( 230, 200, 220, 230 ) );
    // ligher, whiter blue
    gradient.setColorAt ( 1, QColor::fromHsv ( 230, 255, 175, 255 ) );
    // also outline thinly with transparent white
    pen.setWidth ( 1 );
    pen = QPen ( QColor::fromRgbF ( 1, 1, 1, .05 ) );
    painter->setPen ( pen );
    painter->setBrush ( gradient );
    // finally draw the colored plot
    painter->drawPath ( plotPath );

    /// emphasize the closest activity to the mouse
    ActivityPlotMap* matched = binSearch ( lastHoverX, mapper.data(), mapper.size() );
    // draw emphasis line in transparent white
    pen = QPen ( QColor::fromRgbF ( 1, 1, 1, .2 ) );
    painter->setPen ( pen );
    painter->drawLine ( matched->xValue, matched->yValue,
                        matched->xValue, TOTALHEIGHT );

    // draw 'selected' date along the bottom (if not on extremes)
    if ( matched->index != mapper.first().index
            && matched->index != mapper.last().index )
    {
        drawDate ( painter, matched->xValue,PLOTBOTTOM + 5,
                   vector.value ( matched->index ).getDate() );
    }

    /// draw extreme dates
    drawDate ( painter, mapper.first().xValue,
               PLOTBOTTOM + 20, vector.value ( mapper.first().index ).getDate() );
    drawDate ( painter, mapper.last().xValue,
               PLOTBOTTOM + 20, vector.value ( mapper.last().index ).getDate() );

    qDebug() << "Paint took " << time.elapsed() <<  "ms";
}



/**
 * This method paints the background of the plot. Its a gradient from right to left,
 * darker gray to lighter.  It also has lines at the 25, 50, and 75th percentiles.
 * It is the height and width of the plot.
 * @param painter 
 */
void ActivityPlot::paintBg ( QPainter* painter )
{
    QLinearGradient gradient ( 0, 0, PLOTRIGHT, 0 );
    gradient.setColorAt ( 0, QColor::fromRgbF ( 0, 0, 0, .4 ) );
    gradient.setColorAt ( 1, QColor::fromRgbF ( 0, 0, 0, .7 ) );

    QBrush brush ( gradient );
    QPen   pen ( QColor::fromRgbF ( 0, 0, 0, 0 ) );   // transparent
    painter->setBrush ( brush );
    painter->setPen ( pen );

    painter->drawRect ( 0, 0, TOTALWIDTH, TOTALHEIGHT );

    pen = QPen ( QColor::fromRgbF ( 0, 0, 0, .2 ) );
    pen.setWidth ( .5 );
    painter->setPen ( pen );

    painter->drawLine ( 0, PLOTBOTTOM / 2, TOTALWIDTH, PLOTBOTTOM / 2 );
    painter->drawLine ( 0, PLOTBOTTOM / 4, TOTALWIDTH, PLOTBOTTOM / 4 );
    painter->drawLine ( 0, 3 * PLOTBOTTOM / 4, TOTALWIDTH, 3 * PLOTBOTTOM / 4 );
}

/**
 * This method paints a date string around a given area, depending on the
 * location of the plot.  Dates are drawn to the right of a supplied point
 * if on the left of the plot, and on the left of a point if on the right side
 * of the plot. Is that confusing or what?
 * @param painter The painter doing the painting.
 * @param x The central axis to draw on either (right or left) side of.
 * @param y The vertical position at which to draw.
 * @param date The date to draw.
 */
void ActivityPlot::drawDate ( QPainter* painter, float x, float y, QDate date )
{
    int flags;
    QRectF rect;
    QPen pen ( QColor::fromRgbF ( 1, 1, 1, .5 ) );

    if ( x < PLOTRIGHT / 2 )
    {
        flags = Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine | Qt::TextDontClip;
        rect = QRectF ( x + 5, y, 30, 20 );
    }
    else
    {
        flags = Qt::AlignRight | Qt::AlignVCenter | Qt::TextSingleLine | Qt::TextDontClip;
        rect = QRectF ( x - 30 - 5, y, 30, 20 );
    }

    painter->setPen ( pen );
    painter->drawText ( rect, flags, date.toString() );
}

/**
 * Like drawDate() but draws a value (a float) varied vertically to a given point,
 * depending on the top or bottom half location. Still confused?
 * @param painter The painter doing the painting.
 * @param x The horizontal position at which to draw.
 * @param y The central vertical axis to draw on either side of
 * @param value The value to draw.
 */
void ActivityPlot::drawValue ( QPainter* painter, float x, float y, float value )
{
    int flags;
    QRectF rect;
    QPen pen ( QColor::fromRgbF ( 1, 1, 1, .5 ) );

    if ( y < PLOTBOTTOM / 2 )
    {
        flags = Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine | Qt::TextDontClip;
        rect = QRectF ( x, y + 12, 30, 10 );
    }
    else
    {
        flags = Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine | Qt::TextDontClip;
        rect = QRectF ( x, y - 12, 30, 10 );
    }

    painter->setPen ( pen );
    painter->drawText ( rect, flags, QString::number ( value, 'g', 2 ) );
}




/**
* This method constructs a subpath for a set of consecutive activities.
* @param path A reference to a path to work on.
* @param vector The set of activities to plot.
*/
void ActivityPlot::subpathGroup ( QPainterPath* path, QVector<ActivityPlotMap> vector )
{
    QPointF startPoint ( vector.first().xValue, ActivityPlot::PLOTBOTTOM );
    QPointF endPoint ( vector.last().xValue,  ActivityPlot::PLOTBOTTOM );

    path->moveTo ( startPoint );

    // if the group has only a single point, draw it as a sort of a triangle,
    // to make it more distinguishable from a thin line
    if ( vector.size() == 1 )
    {
        // if this single point is at the start of the plot
        if ( vector.first().xValue == ActivityPlot::PLOTLEFT )
        {
            path->lineTo ( vector.first().xValue, vector.first().yValue );
            path->lineTo ( vector.first().xValue + xInterval / 2, ActivityPlot::PLOTBOTTOM );
            path->lineTo ( endPoint );
            return;
        }
        // if this single point is at the end of the plot
        else if ( vector.first().xValue == ActivityPlot::PLOTRIGHT )
        {
            path->lineTo ( vector.first().xValue - xInterval / 2, ActivityPlot::PLOTBOTTOM );
            path->lineTo ( vector.first().xValue, vector.first().yValue );
            path->lineTo ( endPoint );
            return;
        }

        // if this single point is somewhere in the middle of the plot (normal)
        path->lineTo ( vector.first().xValue - xInterval / 2, ActivityPlot::PLOTBOTTOM );
        path->lineTo ( vector.first().xValue, vector.first().yValue );
        path->lineTo ( vector.first().xValue + xInterval / 2, ActivityPlot::PLOTBOTTOM );
        path->lineTo ( endPoint );
        return;
    }


    int i;
    // else, draw a normal group.
    for ( i = 0; i < vector.size(); i++ )
    {
        ActivityPlotMap cur = vector.value ( i );
        path->lineTo ( cur.xValue, cur.yValue );
    }

    path->lineTo ( endPoint );
    path->lineTo ( startPoint );
}





/**
* An implementation of a sort of binary search.
* This assumes that the data is ordered by xValue.
* It returns a reference to the ActivityPlotMap that is located closest to the
* value supplied.
* @param value The needle to be searched for, probably an xValue.
* @param data An array of ActivityPlotMaps to seach through, ordered by xValues.
* @param len The length of the array supplied.
* @return A reference to the matched ActivityPlotMap.
*/
ActivityPlotMap* ActivityPlot::binSearch ( float value, ActivityPlotMap* data, int len )
{
    // if were in the last partition, decide between the two endpoints
    if ( len == 1 )
    {
        if ( value == data->xValue )
        {
            return data;
        }

        // check if its the second half
        if ( ( data + 1 )->xValue - value < value - data->xValue )
        {
            return ( data + 1 );
        }
        // else its the first half
        else
        {
            return data;
        }
    }

    // otherwise, re-partition and search again
    int mid = len / 2;
    // if value is in first half
    if ( ( data + mid )->xValue > value )
    {
        // search first half of set
        return binSearch ( value, data, mid );
    }
    // if value is in second half
    else if ( ( data + mid )->xValue < value )
    {
        // search second half
        return binSearch ( value, ( data + mid ), len - mid );
    }
    // else, this must be the value
    // i dont think we ever get this far...
    else
    {
        return ( data + mid );
    }
}

/**
* Handles mouse press events.
* Finds the closest activity to the mouse point and emits a focused signal.
* @param event The mouseEvent.
*/
void ActivityPlot::mousePressEvent ( QGraphicsSceneMouseEvent * event )
{

    ActivityPlotMap* matched = binSearch ( event->pos().x(), mapper.data(), mapper.size() );
    emit focused ( vector.value ( matched->index ).getId() );
}

/**
* Handles mouseovers by setting an internal variable storing the location.
* Afterwards, updates the display.
* @param event The mouse event.
*/
void ActivityPlot::hoverMoveEvent ( QGraphicsSceneHoverEvent * event )
{
    lastHoverX = event->pos().x();
    //TODO: maybe dont update this often?
    update();
}

